package br.cin.stadium.middleware.event;

import java.util.ArrayList;


import br.cin.stadium.middleware.StadiumException;
import br.cin.stadium.middleware.directory.ServerLocation;
import br.cin.stadium.middleware.marshall.MarshallOutputStream;
import br.cin.stadium.middleware.marshall.Marshaller;
import br.cin.stadium.middleware.media.MediaManager;
import br.cin.stadium.middleware.media.MediaReference;
import br.cin.stadium.middleware.network.DataOutputStream;
import br.cin.stadium.middleware.network.NetworkException;
import br.cin.stadium.middleware.network.NetworkHandler;
import br.cin.stadium.middleware.util.Logger;

/**
 * This manager controls all event communications between network and others layers
 * that needs exchanges data.
 * <br>
 * The means idea: allow Synchronous and ASynchronous calls.
 * <br>
 *  - Synchronous : doAction method calls directly according Event parameters
 *  <br>
 *  - ASynchronous: requestAction add in EventQueue that is read by another thread.
 * <br>
 * @author Fabiano, Jorge, Rafael and Vando
 */
public class EventManager implements Runnable {
	
	/**
	 * EventQueue used in ASynchronous calls.
	 */
	private ArrayList<EventWrapper> eventQueue;
	
	
	/**
	 * Used as a monitor to control asynchronous calls.
	 */
	//private Object lock;
	
	/**
	 * How many that the same event can appers in the queue.
	 */
	private static int EVENT_LIMIT_PROCESS = 3;
	
	/**
	 * EventManager unique instance - Singleton Pattern
	 */
	private static EventManager instance;
	
	/**
	 * Private Constructor
	 */
	private EventManager() {
		this.eventQueue = new ArrayList<EventWrapper>();
		//this.lock = new Object();
		new Thread(this).start();
	}
	
	/**
	 * getInstance method - to implements Singleton Pattern
	 * @return EventManager unique instance
	 */
	public static EventManager getInstance() {
		if(instance == null) {
			instance = new EventManager();
		}
		return instance;
	}
	
	/**
	 * Receive an event to be performed as an asynchronous call
	 * 
	 * @param event Event to be processed
	 * @param receiver Receiver that will handle event call response
	 */
	public void requestAction(EventMiddleware event, EventCallbackReceiver receiver) {
		Logger.log("EventManager", "requestAction", "Begin");

		this.requestAction(new EventWrapper(event, receiver));

		Logger.log("EventManager", "requestAction", "END");
	}
	
	/**
	 * Receive an event to be performed as an synchronous call
	 * 
	 * @param event Event to be processed
	 */
	public void doAction(EventMiddleware event) throws StadiumException {
		Logger.log("EventManager", "doAction", "BEGIN (" + event.getOpCode() + ")");
		
		switch(event.getOpCode()) {
		case Event.OPCODE_BIND:
		case Event.OPCODE_UNBIND:
			try {
					ServerLocation sl = (ServerLocation)event.getParameter()[1];
					MarshallOutputStream mos = new DataOutputStream();
					mos.write(event.getData());
					mos.write(Marshaller.marshall(sl));
					
					NetworkHandler.getInstance().sendData(new Object[]{(ServerLocation)event.getParameter()[0]}, mos.getData());
					
				} catch (NetworkException ne) {
					
					Logger.logError("EventManager", "doAction", "Error-ne", ne);
					throw new StadiumException("Error in BIND/UNBIND process. Please try again.");
				} finally {
					
					NetworkHandler.getInstance().closeConnection();
					
					if(event.getOpCode() == Event.OPCODE_BIND) {
						try {
							NetworkHandler.getInstance().start();
						} catch (NetworkException ne) {
							Logger.logError("NetworkHandler", "sendData", "Error-ne-finally", ne);
							throw new StadiumException("Bind/Unbind Done - but it was not possible to listenning for connection");
						}
					}
				}
				
			break;
		}
		Logger.log("EventManager", "doAction", "END");
	}
	
	/**
	 * Use EventWrapper class to store event,receiver in the queue
	 * @param ew Event,Receive tuple
	 */
	private void requestAction(EventWrapper ew) {
		synchronized (this.eventQueue) {
			this.eventQueue.add(ew);
			this.eventQueue.notifyAll();
		}
	}
	
	
	/* (non-Javadoc)
	 * @see java.lang.Runnable#run()
	 */
	public void run() {
		Logger.log("EventManager", "run", "Begin");
		
		while(true) {
			Logger.log("EventManager", "run", "WHILE");

			if(eventQueue.size() > 0) {
				Logger.log("EventManager", "run", "[eventQueue.size() > 0] = " + eventQueue.size());

				boolean success = true;

				EventWrapper ew = eventQueue.remove(0);
				EventMiddleware e = ew.getEvent();
				e.setCounter(e.getCounter() + 1);
				

				switch (e.getOpCode()) {
				case Event.OPCODE_READ_FROM_NAME:
					Logger.log("OPCODE_READ_FROM_NAME");

					byte[] data = Marshaller.marshall(MediaManager.getInstance().searchForMediaReference(e.getOpCode(), e.getParameter()[0]));
					e.setData(data);
					ew.getReceiver().handleEvent(e);
					
					break;
				case Event.OPCODE_READ_FROM_SECTOR:
					Logger.log("OPCODE_READ_FROM_SECTOR");

					int sector = Integer.parseInt(e.getParameter()[0].toString());
					
					byte[] data2 = Marshaller.marshall(MediaManager.getInstance().searchForMediaReference(e.getOpCode(), sector));
					e.setData(data2);
					ew.getReceiver().handleEvent(e);

					break;
					
				case Event.OPCODE_READ_ALL_MEDIA:
					Logger.log("OPCODE_READ_ALL_MEDIA");

					byte[] data1 = Marshaller.marshall(MediaManager.getInstance().searchForMediaReference(e.getOpCode(), null));
					e.setData(data1);
					ew.getReceiver().handleEvent(e);
					
					break;
				case Event.OPCODE_REQUEST_MEDIA:
					Logger.log("OPCODE_REQUEST_MEDIA");

					byte[] data3 = Marshaller.marshall(MediaManager.getInstance().searchForMedia(e.getParameter()[0].toString()));
					
					e.setData(data3);
					ew.getReceiver().handleEvent(e);

					break;
					
				case Event.OPCODE_RETRIEVE_DEVICES:
					Logger.log("OPCODE_RETRIEVE_DEVICES");

					Object[] params = e.getParameter();
					byte[] data4 = NetworkHandler.createConnection(params[0].toString(), Integer.parseInt(params[1].toString()));
					
					e.setData(data4);
					ew.getReceiver().handleEvent(e);

					break;
					
				case Event.OPCODE_GET_IP:
					Logger.log("OPCODE_GET_IP");

					String data5 = NetworkHandler.getInstance().getMyIp();
					
					e.setData(data5.getBytes());
					ew.getReceiver().handleEvent(e);

					break;
					
				case Event.OPCODE_NEW_WRITE:
					Logger.log("OPCODE_NEW_WRITE");
					
					//FROM APP TO NETWORK
					if(e.getSocketId() == -1) {
						try {
							Logger.log("NEW ENTRY IN REPOSITORY - NOTIFY OTHERS");
							
							DataOutputStream mdos = new DataOutputStream();
							mdos.writeInt(Event.OPCODE_NEW_WRITE);
							mdos.writeInt(e.getData().length);
							mdos.write(e.getData());
							
							NetworkHandler.getInstance().sendData(e.getParameter(), mdos.getData());
						} catch (Exception e2) {
							Logger.logError("Error while notify about new Write", e2);
						}
						
					} 
					//FROM NETWORK TO APP
					else {
						Logger.log("NEW ENTRY IN REPOSITORY - PLEASE REFRESH DATA");
						
						MediaReference ref = new MediaReference();
						Marshaller.unmarshall(ref, e.getData());
						ref.getServerLocation().setIp((String)e.getParameter()[0]);
						EventRemote er = new EventRemote(Event.OPCODE_NEW_WRITE, ref);
						
						MediaManager.getInstance().notifyListeners(er);
					}

					break;
					
				default:
					Logger.log("Op code not valid: " + e.getOpCode());
					break;
				}
				
				if(!success) {
					if(e.getCounter() < EVENT_LIMIT_PROCESS) {
						this.requestAction(ew);
					}
					
				}
				
			} else {
				try {
					synchronized (this.eventQueue) {
						this.eventQueue.wait();
					}
				} catch (InterruptedException e) {
					Logger.logError("ERRO NO WAIT",e);
					// TODO Auto-generated catch block
				}
			}
		}

	}

	
	/**
	 * Used only to encapsulate Event and EventCallbackReceiver
	 * @author Fabiano, Jorge, Rafael and Vando
	 */
	private class EventWrapper {
		private EventMiddleware event;
		private EventCallbackReceiver callBackReceiver;
		
		public EventWrapper(EventMiddleware event, EventCallbackReceiver receiver) {
			this.event = event;
			this.callBackReceiver = receiver;
		}
		
		public EventMiddleware getEvent() {
			return this.event;
		}
		
		public EventCallbackReceiver getReceiver() {
			return this.callBackReceiver;
		}
	}

	
}
	

